/*
 * Copyright (C) 2007 by Quentin Anciaux
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Library General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; if not, write to the Free Software Foundation,
 * Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *	@author Quentin Anciaux
 */
package org.allcolor.yahp.converter;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.allcolor.yahp.converter.IHtmlToPdfTransformer.CConvertException;

//import org.apache.log4j.PropertyConfigurator;

/**
 * This class convert an HTML input to a PDF.
 * 
 * @author Quentin Anciaux
 * @version 0.94
 */
public final class CYaHPConverter {
	static {
		try {
			System.setProperty("file.encoding", "utf-8");
		} catch (Exception ignore) {
		}
	}
	/** an handle to a mutex object for thread safety */
	private static final CMutex countMutex = new CMutex();

	/** Number of active instance */
	private static int countInstance = 0;

	/** an handle to a mutex object for thread safety */
	private final CMutex mutex = new CMutex();

	/** an handle to the known transformers */
	private Map transformers = new HashMap();

	/**
	 * Creates a new CYaHPConverter object.
	 */
	public CYaHPConverter() {
		this(true);
	} // end CYaHPConverter()

	private final boolean useClassLoader;

	/**
	 * Creates a new CYaHPConverter object.
	 */
	public CYaHPConverter(boolean useClassLoader) {
		this.useClassLoader = useClassLoader;
		try {
			countMutex.acquire();
			countInstance++;
			init(useClassLoader);
		} // end try
		finally {
			try {
				countMutex.release();
			} // end try
			catch (final Exception ignore) {
			}
		} // end finally
	} // end CYaHPConverter()

	/**
	 * Delete the jar file from the temp directory Destroy the classloader
	 */
	public static void destroy() {
		try {
			countMutex.acquire();
			for (int i = 0; i < fileToDeleteOnDestroy.size(); i++) {
				File f = (File) fileToDeleteOnDestroy.get(i);
				if (f != null && f.exists())
					try {
						f.delete();
					} catch (Exception ignore) {
					}
			}
			CClassLoader.destroy();
		} // end try
		finally {
			countInstance = 0;

			try {
				countMutex.release();
			} // end try
			catch (final Exception e) {
			}
		} // end finally
	} // end destroy()

	private static List fileToDeleteOnDestroy = new ArrayList();

	public static void registerFileToDeleteOnDestroy(File f) {
		fileToDeleteOnDestroy.add(f);
	}

	/**
	 * Convert the document pointed by url in a PDF file. This method is thread
	 * safe.
	 * 
	 * @param url
	 *            Url to the document
	 * @param size
	 *            PDF Page size
	 * @param hf
	 *            header-footer list
	 * @param out
	 *            outputstream to render into
	 * @param fproperties
	 *            properties map
	 * 
	 * @throws CConvertException
	 *             if an unexpected error occurs.
	 */
	public final void convertToPdf(final URL url, final IHtmlToPdfTransformer.PageSize size, final List hf,
			final OutputStream out, final Map fproperties) throws CConvertException {
		Map properties = (fproperties != null) ? fproperties : new HashMap();
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		int priority = Thread.currentThread().getPriority();

		try {
			Thread.currentThread().setContextClassLoader(
					new URLClassLoader(new URL[0], this.useClassLoader ? CClassLoader.getLoader("/main") : this.getClass()
							.getClassLoader()));
			Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

			String uri = url.toExternalForm();
			if (uri.indexOf("://") != -1) {
				String tmp = uri.substring(uri.indexOf("://") + 3);
				if (tmp.indexOf('/') == -1) {
					uri += "/";
				}
			}
			uri = uri.substring(0, uri.lastIndexOf("/") + 1);

			if (uri.startsWith("file:/")) {
				uri = uri.substring(6);

				while (uri.startsWith("/")) {
					uri = uri.substring(1);
				} // end while

				uri = "file:///" + uri;
			} // end if

			try {
				IHtmlToPdfTransformer transformer = getTransformer(properties);
				transformer.transform(url.openStream(), uri, size, hf, properties, out);
			} // end try
			catch (final CConvertException e) {
				throw e;
			} catch (final Exception e) {
				throw new CConvertException(e.getMessage(), e);
			} // end catch
		} // end try
		finally {
			Thread.currentThread().setContextClassLoader(loader);
			Thread.currentThread().setPriority(priority);
		} // end finally
	} // end convertToPdf()

	/**
	 * Convert the document in content in a PDF file. This method is thread
	 * safe.
	 * 
	 * @param content
	 *            the html document as a string
	 * @param size
	 *            PDF Page size
	 * @param hf
	 *            header-footer list
	 * @param furlForBase
	 *            base url of the document, mandatory, must end with a '/'
	 * @param out
	 *            outputstream to render into
	 * @param fproperties
	 *            properties map
	 * 
	 * @throws CConvertException
	 *             if an unexpected error occurs
	 */
	public final void convertToPdf(final String content, final IHtmlToPdfTransformer.PageSize size, final List hf,
			final String furlForBase, final OutputStream out, final Map fproperties) throws CConvertException {
		String urlForBase = furlForBase;
		Map properties = (fproperties != null) ? fproperties : new HashMap();

		if (urlForBase != null) {
			try {
				new URL(urlForBase);
			} catch (final Exception e) {
				throw new CConvertException("urlForBase must be a valid URI.", null);
			} // end catch
			if (urlForBase.indexOf("://") != -1) {
				String tmp = urlForBase.substring(urlForBase.indexOf("://") + 3);
				if (tmp.indexOf('/') == -1) {
					urlForBase += "/";
				}
			}
		} // end if

		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		int priority = Thread.currentThread().getPriority();
		try {
			Thread.currentThread().setContextClassLoader(
					new URLClassLoader(new URL[0], this.useClassLoader ? CClassLoader.getLoader("/main") : this.getClass()
							.getClassLoader()));
			Thread.currentThread().setPriority(Thread.MIN_PRIORITY);

			try {
				IHtmlToPdfTransformer transformer = getTransformer(properties);
				transformer.transform(new ByteArrayInputStream(content.getBytes("utf-8")), urlForBase, size, hf, properties, out);
			} // end try
			catch (final CConvertException e) {
				throw e;
			} catch (final Exception e) {
				throw new CConvertException(e.getMessage(), e);
			} // end catch
		} // end try
		finally {
			Thread.currentThread().setContextClassLoader(loader);
			Thread.currentThread().setPriority(priority);
		} // end finally
	} // end convertToPdf()

	/**
	 * If instance count fall to 0, Delete the jar file from the temp directory,
	 * Destroy the classloader by calling the destroy method
	 * 
	 * @throws Throwable
	 *             if an unexpected error occurs
	 */
	protected final void finalize() throws Throwable {
		super.finalize();

		try {
			countMutex.acquire();
			countInstance--;

			if (countInstance == 0) {
				destroy();
			} // end if
		} // end try
		finally {
			try {
				countMutex.release();
			} // end try
			catch (final Exception e) {
			}
		} // end finally
	} // end finalize()

	/**
	 * initialize the classloader, and the transformer
	 */
	private final void init(boolean useClassLoader) {
		final CYaHPConverter converter = this;
		long time = System.currentTimeMillis();

		if (!useClassLoader) {
			return;
		}

		// if classloader is init return.
		if (CClassLoader.getRootLoader().isInit()) {
			return;
		} // end if
			// ensure the temporary files are delete on exit.
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				try {
					converter.finalize();
				} // end try
				catch (final Throwable e) {
				}
			} // end run()
		} // end new
				);

		ClassLoader loader = this.getClass().getClassLoader();
		CClassLoaderConfig config = new CClassLoaderConfig();
		config.addLoaderInfo("/main", new CClassLoaderConfig.CLoaderInfo(true, true, false, false));
		config.addFile("/main", loader.getResource("itext-yahp.jar"));
		config.addFile("/main", loader.getResource("tidy-yahp.jar"));
		config.addFile("/main", loader.getResource("log4j-yahp.jar"));
		config.addFile("/main", loader.getResource("shanijar-yahp.jar"));
		config.addFile("/main", loader.getResource("xmlapi-yahp.jar"));
		config.addFile("/main", loader.getResource("yahp-internal.jar"));
		config.addFile("/main", loader.getResource("commonio-yahp.jar"));
		config.addFile("/main", loader.getResource("commonlog-yahp.jar"));
		config.addFile("/main", loader.getResource("core-renderer-yahp.jar"));
		config.addFile("/main", loader.getResource("jaxen-yahp.jar"));
		CClassLoader.init(config);
		try {
			URL url = CClassLoader.getRootLoader().getResource("log4j.properties");
			if (url != null) {
				ClassLoader cx = Thread.currentThread().getContextClassLoader();
				try {
					Thread.currentThread().setContextClassLoader(CClassLoader.getRootLoader());
					Class pc = CClassLoader.getRootLoader().loadClass("org.apache.log4j.PropertyConfigurator");
					Method configure = pc.getDeclaredMethod("configure", new Class[] { URL.class });
					configure.invoke(null, new Object[] { url });
				} finally {
					Thread.currentThread().setContextClassLoader(cx);
				}
			}
		} catch (Exception ignore) {
		}
	} // end init()

	/**
	 * Get a transformer.
	 * 
	 * @param properties
	 *            The properties map.
	 * 
	 * @return a transformer corresponding to the criterias found in the
	 *         properties map.
	 */
	private IHtmlToPdfTransformer getTransformer(final Map properties) {
		try {
			mutex.acquire();
			IHtmlToPdfTransformer transformer = null;
			String rendererClassName = IHtmlToPdfTransformer.DEFAULT_PDF_RENDERER;

			if ((properties != null) && properties.containsKey(IHtmlToPdfTransformer.PDF_RENDERER_CLASS)) {
				rendererClassName = (String) properties.get(IHtmlToPdfTransformer.PDF_RENDERER_CLASS);
			} // end if

			if (transformers.containsKey(rendererClassName)) {
				transformer = (IHtmlToPdfTransformer) transformers.get(rendererClassName);
			} // end if
			else {
				ClassLoader bootStrap = this.useClassLoader ? CClassLoader.getLoader("/main") : this.getClass().getClassLoader();

				try {
					ClassLoader cx = Thread.currentThread().getContextClassLoader();
					try {
						Thread.currentThread().setContextClassLoader(
								this.useClassLoader ? CClassLoader.getRootLoader() : this.getClass().getClassLoader());
						transformer = (IHtmlToPdfTransformer) bootStrap.loadClass(rendererClassName).newInstance();
						transformers.put(rendererClassName, transformer);
					} finally {
						Thread.currentThread().setContextClassLoader(cx);
					}
				} // end try
				catch (final Exception e) {
					e.printStackTrace();
					System.err.println("SEVERE: Error while getting transformer '" + rendererClassName + "' ! : "
							+ e.getMessage());

					return null;
				} // end catch
			} // end else

			return transformer;
		} finally {
			try {
				mutex.release();
			} // end try
			catch (final Exception e) {
			}
		} // end finally

	} // end getTransformer()
} // end CYaHPConverter
